Leveraging Keys
Video Summary
Let's suppose we're building a UI where we have a big displayed price, and it changes when the user selects a new plan:
The transition between prices is a bit abrupt, and so we want to add a slick animation:
We can accomplish this animation with a CSS keyframe animation. I've taken the liberty of creating an animation, but it isn't working properly. It's only running on page load, not when the plan is changed.
CSS keyframe animations run ASAP, when either:
- The DOM node is first created, or
- The CSS class is added to an existing DOM node.
This lesson is all about how to solve this problem. Below, you'll find a playground that contains all of the starter code you need. We review the code in depth in this video, so please review the video if you're not sure how it works.
Here's the playground from the video. Spend up to 15 minutes seeing if you can re-trigger the CSS keyframe animation whenever the price
prop changes:
Code Playground
Alright, let's talk about how I'd solve this problem today:
Video Summary
Here's what the solution looks like:
// PriceDisplay.jsfunction PriceDisplay({ price }) { const formattedPrice = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(price); \n\ return ( <div className={styles.wrapper}>- <div className={styles.animated}>+ <div key={price} className={styles.animated}> {formattedPrice} </div> </div> );}
We're adding a key
to the element that applies the animated
class. Somewhat miraculously, this does exactly what we need, and the code works perfectly!
But how does this work?
So far in this course, we've seen keys in scenarios like this:
function App() { return ( <ul> {data.map(contact => ( <ContactCard key={contact.id} name={contact.name} job={contact.job} email={contact.email} /> ))} </ul> );}
This is the most common use case for the key
attribute. We need to add a key here, otherwise we'll get a console warning.
Keys allow React to connect a specific ID with a specific React element and the associated DOM. When the data changes, React uses the keys to work out what DOM manipulation is required (whether we edit the contents of a DOM node, reorder the nodes, or destroy/recreate it).
This is difficult to explain in the summary; I suggest watching this video to get a clearer explanation!
In our PriceDisplay
component, we're setting key
equal to the price:
<div key={price} className={styles.animated}> {formattedPrice}</div>
Suppose that the price starts as 0
. That means the key is equal to "0"
(keys must be strings, and so the number 0 is converted into a string).
Then, let's suppose the price changes to 100
. The key updates to "100"
.
By changing the key, we're telling React that this is a fundamentally-different element. Instead of tweaking the existing DOM, it will replace it. The current <div>
is removed from the DOM, and a brand new one is created. And because it's a new node, the animation runs again.
When I first learned of this approach, it felt a bit sneaky to me. Keys are meant to help us render arrays, right? By using them in this way, are we taking advantage of some undocumented feature that could change in a future version of React?
Not at all. It turns out, this is the approach that the React team recommends we take to solve these sorts of problems.
It's also not some clever trick; we're using keys in exactly the way they're intended to be used! Keys are a much more generic tool than we often think, and this is 100% a valid use case for them.
As discussed in the video above, here's the full solution:
// PriceDisplay.jsfunction PriceDisplay({ price }) { const formattedPrice = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(price);
return ( <div className={styles.wrapper}> <div key={price} className={styles.animated}> {formattedPrice} </div> </div> );}
Alternative approaches
We've seen how the key
attribute can help us solve this problem, but how else might we approach this sort of problem?
Let's discuss.
Video Summary
In this video, we walk through an alternative approach. It looks like this:
function PriceDisplay({ price }) { const [applyAnimation, setApplyAnimation] = React.useState(false);
React.useEffect(() => { setApplyAnimation(false);
window.setTimeout(() => { setApplyAnimation(true); }, 0); }, [price]);
const formattedPrice = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(price);
return ( <div className={styles.wrapper}> <div className={applyAnimation ? styles.animated : undefined}> {formattedPrice} </div> </div> );}
The strategy here is:
- Use a state variable,
applyAnimation
to control whetherstyles.animated
is being applied to the<div>
. - Add an effect, with a dependency on the
price
prop - When
price
changes, setapplyAnimation
to false. Let a single frame pass, and then flip it back to true
This strategy mostly works, but occasionally, the number updates in the UI a little too quickly:
Aside from that, though, what do we think of this approach?
I hope you'll agree, it's quite a bit more mental overhead. We have to spend more time trying to understand what's actually going on here.
(It might not feel more complicated, if you're reasonably comfortable with state/effects and are still a little mystified by keys. But hopefully that changes once we get more comfortable with keys!)
The other issue is that we're deviating from established best practices. The useEffect
hook is designed to handle side effects. In practice, this often involves synchronizing some React state with something outside of React. For example:
- Fetching some data over the network, to sync React state with some data in a database somewhere.
- Adding a window-level event listener to sync React state with the window's scroll position.
In this case, we're synchronizing the applyAnimation
state variable with the plan
state variable. And whenever we're synchronizing React with itself, it's usually a sign that we're not doing something in the most conventional / optimal way.
So, for those issues, I think the key
approach is better.
But what about performance? You might be thinking that the key
approach must be slower. After all, we're destroying and recreating an entire DOM node, instead of changing the className
! Surely it's faster to tweak a single attribute, compared with creating a whole new DOM node!
Two things about this:
- Both options in this case are so quick, they round to “instantaneous”. The user won't be able to perceive any performance difference between the two approaches. And so it doesn't really matter
- We might be doing less DOM manipulation in this alternative approach, but consider how much more work we're doing on the React side. The
PriceDisplay
component renders three times every time theplan
changes. It might actually be that the key approach is faster!
The new React docs has a must-read page called “You Might Not Need An Effect”. I highly recommend spending a few minutes going through it.
It even covers the same key approach we learned about in this lesson, under “Resetting all state when a prop changes”.